// 
// RosterWrapper.cs
// 
// Copyright © 2009 Jiří Zárevúcky <zarevucky.jiri@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//

using System;
using System.Threading;
using Galaxium.Core;
using Galaxium.Protocol.Xmpp.Library;
using Galaxium.Protocol.Xmpp.Library.Core;
using Galaxium.Protocol.Xmpp.Library.Utility;

using Anculus.Core;

// TODO: only dispatch when necesarry

namespace Galaxium.Protocol.Xmpp
{
	public class RosterWrapper
	{		
		private IGroup _not_in_roster_group;
		
		private bool _initial = true;
		private XmppSession _session;
		private Roster _roster;
		
		private bool _disable_notification;

		public bool DisableNotification {
			set { _disable_notification = value; }
		}
		
		public ContactCollection Contacts { get; private set; }
		public GroupCollection   Groups   { get; private set; }
		
		public IGroup NotInRosterGroup {
			get {
				if (_not_in_roster_group == null) {
					_not_in_roster_group = new XmppNotInRosterGroup (_session);
					Groups.Add (_not_in_roster_group);
					_session.EmitGroupAdded (new GroupEventArgs (_not_in_roster_group));
				}
				return _not_in_roster_group;
			}
		}
		
		public RosterWrapper (XmppSession session, Roster roster)
		{
			_session = session;
			_roster = roster;

			Contacts = new ContactCollection ();
			Groups = new GroupCollection ();

			_roster.PresenceUpdated += (s, e) =>
				ThreadUtility.Dispatch (() => HandlePresenceUpdated (e.Contact, e.Presence));

			_roster.ContactUpdated += (s, e) =>
				ThreadUtility.Dispatch (() => HandleContactUpdated (e.Contact));
			
			_roster.ContactAdded += (s, e) =>
				ThreadUtility.Dispatch (() => HandleContactAdded (e.Contact));

			_roster.ContactRemoved += (s, e) =>
				ThreadUtility.Dispatch (() => HandleContactRemoved (e.Contact));
			
			_roster.ContactNameChanged += (s, e) =>
				ThreadUtility.Dispatch (() => HandleContactNameChanged (e.Contact, e.OldName));
			
			_roster.ContactAddedToGroup += (s, e) =>
				ThreadUtility.Dispatch (() => HandleContactAddedToGroup (e.Contact, e.Group));
			
			_roster.ContactRemovedFromGroup += (s, e) =>
				ThreadUtility.Dispatch (() => HandleContactRemovedFromGroup (e.Contact, e.Group));

			_roster.SubscriptionRequested += (s, e) =>
				ThreadUtility.Dispatch (() => {
					var act = new SubscriptionRequestedActivity (_session,
					                                             GetContact (e.Contact),
					                                             e.Message);
					ActivityUtility.EmitActivity (this, act);
				});
			
			_roster.SubscriptionUpdated += (s, e) =>
				ThreadUtility.Dispatch (() => HandleSubscriptionUpdated (e.Contact, e.OldState));
			
			_roster.ContactsReceived += (s, e) => 
				ThreadUtility.Dispatch (SetLoggingIn);
			
			_roster.ContactAvatarChanged += (s, e) =>
				ThreadUtility.Dispatch (() => HandleContactAvatarChanged (e.Contact, e.Type, e.Data));
			
		//	OnContactChanged (ContactEventArgs args);
		//	OnContactReverse (ContactEventArgs args);
		//	OnContactDisplayImageChanged (EntityChangeEventArgs args);
		}
		
		private void SetLoggingIn ()
		{
			// This will cause the wrapper to not give any status change notifications
			// for the first 5 seconds. There is no mechanism to figure
			// out when the presence flood ends.
			
			var login_timeout = 5000;
			
			_initial = false;
			_disable_notification = true;
			
			Timer timer = null;
			
			TimerCallback callback = delegate {
				_disable_notification = false;
				timer.Dispose ();
				timer = null;
			};
			
			timer = new Timer (callback, null, login_timeout, -1);
		}
				  
		private void HandleContactUpdated (Contact contact)
		{
			bool not_in_roster = IsNotInRoster (contact);
			
			if (!contact.IsInRoster) {
				AddToNotInRoster (contact);
				var activity = new ContactRemovedActivity (_session, contact.Jid);
				ActivityUtility.EmitActivity (this, activity);
			} else if (not_in_roster && contact.IsInRoster) {
				RemoveFromNotInRoster (contact);
				if (!_initial) {
					var activity = new ContactAddedActivity (_session, GetContact (contact));
					ActivityUtility.EmitActivity (this, activity);
				}
			}
		}
		
		private XmppContact GetContact (Contact item)
		{
			return Contacts.GetContact (item.Jid) as XmppContact;
		}

		private void HandleSubscriptionUpdated (Contact item, SubscriptionState old_state)
		{
			HandlePresenceUpdated (item, null);  // to switch between offline/unknown
			
			if (_initial) return;
			
			IActivity activity = null;
			
			if ((old_state == SubscriptionState.Both
			     || old_state == SubscriptionState.To)
			    && !item.IsSubscribed)
				activity = new SubscriptionDeclinedActivity (_session, GetContact (item));
			
			if ((old_state == SubscriptionState.None
			     || old_state == SubscriptionState.From)
			    && item.IsSubscribed)
				activity = new SubscriptionApprovedActivity (_session, GetContact (item));
				
			if (activity != null)
				ActivityUtility.EmitActivity (this, activity);
		}
		
		private void HandleContactAdded (Contact item)
		{
			Log.Debug ("Contact " + item.Jid + " was added");
			
			var contact = new XmppContact (_session, item.Jid);
			Contacts.Add (contact);
			
			AddToNotInRoster (item);

			HandlePresenceUpdated (item, null);  // to switch between offline/unknown
		}

		private void HandleContactRemoved (Contact contact)
		{
			Log.Debug ("Contact " + contact.Jid + " was removed");
			
			var xmpp_contact = Contacts.GetContact (contact.Jid);
			RemoveFromNotInRoster (contact);
			Contacts.Remove (xmpp_contact);
		}
		
		private void HandleContactNameChanged (Contact item, string old_name)
		{
			var contact = Contacts.GetContact (item.Jid);
			contact.Nickname = item.Name;
			var args = new EntityChangeEventArgs<string> (contact, item.Name, old_name);
			_session.EmitContactNameChanged (args);
		}

		private void HandleContactAddedToGroup (Contact item, string group)
		{
			Log.Debug ("Contact " + item.Jid + " added to group " + group);
			
			XmppGroup xmpp_group;
			if (Groups.GroupExists ("R_" + group)) {
				xmpp_group = Groups.GetGroup ("R_" + group) as XmppGroup;
			} else {
				xmpp_group = new XmppGroup (_session, group);
				Groups.Add (xmpp_group);
				_session.EmitGroupAdded (new GroupEventArgs (xmpp_group));
			}

			var contact = GetContact (item);
			xmpp_group.Add (contact);
			var args = new ContactListEventArgs (contact, xmpp_group);
			_session.EmitContactAdded (args);
		}

		private void HandleContactRemovedFromGroup (Contact item, string group)
		{
			Log.Debug ("Contact " + item.Jid + " removed from group " + group);
			
			var contact = GetContact (item);
			var xmpp_group = Groups.GetGroup ("R_" + group);
			xmpp_group.Remove (contact);
			var args = new ContactListEventArgs (contact, xmpp_group);
			_session.EmitContactRemoved (args);

			if (xmpp_group.Count == 0) {
				Groups.Remove (xmpp_group);
				_session.EmitGroupRemoved (new GroupEventArgs (xmpp_group));
			}
		}
		
		private void HandlePresenceUpdated (Contact item, Presence pres)
		{
			var contact = GetContact (item);
			var old_presence = contact.Presence as XmppPresence;
			var presence = XmppPresence.Get (StatusMethods.GetStatus (item));
			var description = item.MainPresence.GetDescription ();
			
			if (presence != contact.Presence) {
				var args = new EntityChangeEventArgs<IPresence> (contact, presence, contact.Presence);
				contact.Presence = presence;
				_session.EmitContactPresenceChanged (args);
			}
			
			if (description != contact.DisplayMessage) {
				var args = new EntityChangeEventArgs<string> (contact, description, contact.DisplayMessage);
				contact.DisplayMessage = description;
				_session.EmitContactDisplayMessageChanged (args);
			}
			
			if (_disable_notification) return;
			var activity = new StatusChangedActivity (_session, contact, old_presence);
			ActivityUtility.EmitActivity (this, activity);
		}
		
		#region "not in roster" contacts handling
		
		private bool IsNotInRoster (Contact contact)
		{
			return NotInRosterGroup.Contains (Contacts.GetContact (contact.Jid));
		}
		
		private void AddToNotInRoster (Contact contact)
		{
			Log.Debug ("Adding \"not in roster\" contact " + contact.Jid);
			
			var group = NotInRosterGroup;
			var xmpp_contact = Contacts.GetContact (contact.Jid);
			group.Add (xmpp_contact);
			_session.EmitContactAdded (new ContactListEventArgs (xmpp_contact, group));
		}
		
		private void RemoveFromNotInRoster (Contact contact)
		{
			Log.Debug ("Removing \"not in roster\" contact " + contact.Jid);
			
			var group = NotInRosterGroup;
			var xmpp_contact = Contacts.GetContact (contact.Jid);
			group.Remove (xmpp_contact);
			_session.EmitContactRemoved (new ContactListEventArgs (xmpp_contact, group));
		}
		
		#endregion
		
		private void HandleContactAvatarChanged (Contact contact, string type, byte[] data)
		{
			var entity = Contacts.GetContact (contact.Jid);
			var old_image = entity.DisplayImage;
			var image = new XmppDisplayImage (data);
			entity.DisplayImage = image;
			
			var args = new EntityChangeEventArgs<IDisplayImage> (entity, image, old_image);
			_session.EmitContactDisplayImageChanged (args);
		}
	}
}
